/*
 * Copyright (C) 2017. Uber Technologies
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.uber.rib.core

import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.uber.autodispose.lifecycle.LifecycleEndedException
import com.uber.autodispose.lifecycle.LifecycleNotStartedException
import com.uber.rib.core.RibEvents.ribActionEvents
import com.uber.rib.core.RibEventsUtils.assertRibActionInfo
import com.uber.rib.core.RibRefWatcher.Companion.getInstance
import com.uber.rib.core.lifecycle.InteractorEvent
import io.reactivex.Completable
import io.reactivex.observers.TestObserver
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

class InteractorAndRouterTest {

  private val childInteractor: Interactor<*, *> = mock()
  private val ribRefWatcher: RibRefWatcher = mock()

  private lateinit var interactor: TestInteractor
  private lateinit var router: TestRouter
  private val ribActionInfoObserver = TestObserver<RibActionInfo>()

  @Before
  fun setup() {
    val presenter: TestPresenter = mock()
    val component: InteractorComponent<TestPresenter, TestInteractor> = mock {
      on { presenter() } doReturn (presenter)
    }
    interactor = TestInteractor(childInteractor)
    router = TestRouter(interactor, component)
    RibEvents.enableRibActionEmissions()
  }

  @Test
  fun attach_shouldAttachChildController() {
    // Given.
    ribActionEvents.subscribe(ribActionInfoObserver)

    // When.
    router.dispatchAttach(null)

    // Then.
    val ribActionInfoValues = ribActionInfoObserver.values()
    ribActionInfoValues
      .last()
      .assertRibActionInfo(
        RibEventType.ATTACHED,
        RibActionEmitterType.INTERACTOR,
        RibActionState.COMPLETED,
        "com.uber.rib.core.InteractorAndRouterTest${'$'}TestInteractor",
      )
    verify(childInteractor).dispatchAttach(null)
  }

  @Test
  fun attach_withoutAllowingEmissions_shouldNotEmtRibActionEvents() {
    // Given.
    RibEvents.areRibActionEmissionsAllowed = false
    ribActionEvents.subscribe(ribActionInfoObserver)

    // When.
    router.dispatchAttach(null)

    // Then.
    assertThat(ribActionInfoObserver.values()).isEmpty()
  }

  @Test
  fun detach_shouldDetachChildController() {
    // Given.
    ribActionEvents.subscribe(ribActionInfoObserver)
    router.dispatchAttach(null)

    // When.
    router.dispatchDetach()

    // Then.
    val ribActionInfoValues = ribActionInfoObserver.values()
    ribActionInfoValues
      .last()
      .assertRibActionInfo(
        RibEventType.DETACHED,
        RibActionEmitterType.ROUTER,
        RibActionState.COMPLETED,
        "com.uber.rib.core.FakeRouter",
      )
    verify(childInteractor).dispatchDetach()
  }

  @Test
  @Throws(Exception::class)
  fun correspondingEvents_whenActive_shouldReturnInactive() {
    Truth.assertThat(interactor.correspondingEvents().apply(InteractorEvent.ACTIVE))
      .isEqualTo(InteractorEvent.INACTIVE)
  }

  @Test(expected = LifecycleEndedException::class)
  @Throws(Exception::class)
  fun correspondingEvents_whenInactive_shouldCrash() {
    interactor.correspondingEvents().apply(InteractorEvent.INACTIVE)
  }

  @Test
  fun saveInstanceState_whenDetached_shouldNotSaveChildControllerState() {
    // When.
    val outState: Bundle = mock()
    interactor.onSaveInstanceStateInternal(outState)

    // Then.
    verify(childInteractor, times(0)).onSaveInstanceStateInternal(outState)
  }

  @Test
  fun childControllers_shouldHaveRightLifecycle() {
    val parentInteractor = TestInteractorA()
    val component: InteractorComponent<TestPresenter, TestInteractorA> =
      object : InteractorComponent<TestPresenter, TestInteractorA> {
        override fun inject(interactor: TestInteractorA) {}
        override fun presenter(): TestPresenter {
          return TestPresenter()
        }
      }
    val router = TestRouterA(parentInteractor, component)
    val parentObserver = RecordingObserver<InteractorEvent>()
    parentInteractor.lifecycle().subscribe(parentObserver)
    router.dispatchAttach(null)
    Truth.assertThat(parentObserver.takeNext()).isEqualTo(InteractorEvent.ACTIVE)
    val childA = TestChildInteractor()
    val childComponent: InteractorComponent<TestPresenter, TestChildInteractor> =
      object : InteractorComponent<TestPresenter, TestChildInteractor> {
        override fun inject(interactor: TestChildInteractor) {}
        override fun presenter(): TestPresenter {
          return TestPresenter()
        }
      }
    val childRouter = TestChildRouter(childA, childComponent)
    val childObserverA = RecordingObserver<InteractorEvent>()
    childA.lifecycle().subscribe(childObserverA)
    router.attachChild(childRouter)
    Truth.assertThat(childObserverA.takeNext()).isEqualTo(InteractorEvent.ACTIVE)
    val childB = TestChildInteractor()
    val childObserverB = RecordingObserver<InteractorEvent>()
    childB.lifecycle().subscribe(childObserverB)
    val childBRouter = TestChildRouter(childB, childComponent)
    childRouter.attachChild(childBRouter)
    Truth.assertThat(childObserverB.takeNext()).isEqualTo(InteractorEvent.ACTIVE)
    router.dispatchDetach()
    Truth.assertThat(parentObserver.takeNext()).isEqualTo(InteractorEvent.INACTIVE)
    Truth.assertThat(childObserverA.takeNext()).isEqualTo(InteractorEvent.INACTIVE)
    Truth.assertThat(childObserverB.takeNext()).isEqualTo(InteractorEvent.INACTIVE)
  }

  @Test
  fun detachChild_whenOneChild_shouldWatchOneDeletedInteractor() {
    val rootInteractor = TestInteractorB()
    val component: InteractorComponent<TestPresenter, TestInteractorB> =
      object : InteractorComponent<TestPresenter, TestInteractorB> {
        override fun inject(interactor: TestInteractorB) {}
        override fun presenter(): TestPresenter {
          return TestPresenter()
        }
      }
    val router = TestRouterB(component, rootInteractor, ribRefWatcher)
    router.dispatchAttach(null)
    val childInteractor = TestInteractorB()
    val childRouter = TestRouterB(childInteractor, component)
    router.attachChild(childRouter)
    verify(ribRefWatcher, never()).watchDeletedObject(any())

    // Action: Detach the child interactor.
    router.detachChild(childRouter)

    // Verify: the reference watcher observes the detached interactor and child.
    verify(ribRefWatcher).watchDeletedObject(eq(childInteractor))
  }

  @Test
  fun detachChild_whenTwoNestedChildren_shouldWatchTwoNestedDeletions() {
    val component: InteractorComponent<TestPresenter, TestInteractorB> =
      object : InteractorComponent<TestPresenter, TestInteractorB> {
        override fun inject(interactor: TestInteractorB) {}
        override fun presenter(): TestPresenter {
          return TestPresenter()
        }
      }
    val rootRouter = TestRouterB(component, TestInteractorB(), ribRefWatcher)
    val child = addTwoNestedChildInteractors()
    verify(ribRefWatcher, never()).watchDeletedObject(any())

    // Action: Detach all child interactors.
    rootRouter.detachChild(child)

    // Verify: called four times. Twice for each interactor.
    verify(ribRefWatcher, times(2)).watchDeletedObject(any())
  }

  @Test
  fun init_useStateFlow_eventIsNotFired() = runTest {
    // Given
    setupInteractorForStateFlow()
    var eventCount = 0
    val lifecycleFlow = interactor.lifecycleFlow

    // When
    withTimeoutOrNull(10) { lifecycleFlow.collect { eventCount++ } }

    // Then
    assertThat(eventCount).isEqualTo(0)
  }

  @Test
  fun init_eventIsNotFired() = runTest {
    // When
    var eventCount = 0
    val lifecycleFlow = interactor.lifecycleFlow

    // When
    withTimeoutOrNull(10) { lifecycleFlow.collect { eventCount++ } }

    // Then
    assertThat(eventCount).isEqualTo(0)
  }

  @Test
  fun requestScope_initAndUseStateFlow_throwsAnError() {
    // Given
    setupInteractorForStateFlow()

    // When
    try {
      interactor.requestScope()
      fail("Expected LifecycleEndedException")
    } catch (e: LifecycleNotStartedException) {
      // Then
      // Expected exception, do nothing
    }
  }

  @Test
  fun requestScope_initWith_throwsAnError() {
    // When
    try {
      interactor.requestScope()
      fail("Expected LifecycleEndedException")
    } catch (e: LifecycleNotStartedException) {
      // Then
      // Expected exception, do nothing
    }
  }

  @Test
  fun requestScope_useStateFlowAndAttached_requestScopeIsNotCompleted() {
    // Given
    setupInteractorForStateFlow()
    interactor.dispatchAttach(null)

    // When
    Completable.wrap(interactor.requestScope())
      .test()
      // Then
      .assertNotComplete()
  }

  @Test
  fun requestScope_attached_requestScopeIsNotCompleted() {
    // Given
    interactor.dispatchAttach(null)

    // When
    Completable.wrap(interactor.requestScope())
      .test()
      // Then
      .assertNotComplete()
  }

  @Test
  fun requestScope_useStateFlowAndDetached_requestScopeIsCompleted() {
    // Given
    setupInteractorForStateFlow()
    interactor.dispatchAttach(null)
    val requestScope = Completable.wrap(interactor.requestScope())
    interactor.dispatchDetach()

    // When
    requestScope
      .test()
      // Then
      .assertComplete()
  }

  @Test
  fun requestScope_detached_requestScopeIsCompleted() {
    // Given
    interactor.dispatchAttach(null)
    val requestScope = Completable.wrap(interactor.requestScope())
    interactor.dispatchDetach()

    // When
    requestScope
      .test()
      // Then
      .assertComplete()
  }

  private fun setupInteractorForStateFlow() {
    RibEvents.useStateFlowInteractorEvent()
    val presenter: TestPresenter = mock()
    val component: InteractorComponent<TestPresenter, TestInteractor> = mock {
      on { presenter() } doReturn (presenter)
    }
    interactor = TestInteractor(childInteractor)
    router = TestRouter(interactor, component)
  }

  private fun addTwoNestedChildInteractors(): Router<TestInteractorB> {
    val component: InteractorComponent<TestPresenter, TestInteractorB> =
      object : InteractorComponent<TestPresenter, TestInteractorB> {
        override fun inject(interactor: TestInteractorB) {}
        override fun presenter(): TestPresenter {
          return TestPresenter()
        }
      }
    router.dispatchAttach(null)
    val childRouter1 = TestRouterB(component, TestInteractorB(), ribRefWatcher)
    val childRouter2 = TestRouterB(component, TestInteractorB(), ribRefWatcher)
    router.attachChild(childRouter1)
    childRouter1.attachChild(childRouter2)
    return childRouter1
  }

  private class TestInteractor(private val mChildInteractor: Interactor<*, *>) :
    Interactor<TestPresenter, Router<TestInteractor>>() {
    val mockedWorker: Worker = mock()
    override fun didBecomeActive(savedInstanceState: Bundle?) {
      super.didBecomeActive(savedInstanceState)
      val router: Router<*> = FakeRouter(mChildInteractor, getInstance(), Thread.currentThread())
      WorkerBinder.bind(this, mockedWorker)
      this.router.attachChild(router)
    }

    override fun onSaveInstanceState(outState: Bundle) {
      super.onSaveInstanceState(outState)
      outState.putString(TEST_KEY, TEST_VALUE)
    }
  }

  private class TestRouter(
    interactor: TestInteractor,
    component: InteractorComponent<TestPresenter, TestInteractor>,
  ) : Router<TestInteractor>(component, interactor, getInstance(), Thread.currentThread()) {
    init {
      interactor.setPresenter(component.presenter())
    }
  }

  private open class TestPresenter : Presenter()

  private class TestRouterA(
    interactor: TestInteractorA,
    component: InteractorComponent<TestPresenter, TestInteractorA>,
  ) : Router<TestInteractorA>(component, interactor, getInstance(), Thread.currentThread()) {
    private var savedInstanceState: Bundle? = null
    override fun dispatchAttach(savedInstanceState: Bundle?, tag: String) {
      super.dispatchAttach(savedInstanceState, tag)
      this.savedInstanceState = savedInstanceState
    }

    init {
      interactor.setPresenter(component.presenter())
    }
  }

  private class TestInteractorA : Interactor<TestPresenter, Router<TestInteractorA>>()
  private class TestInteractorB : Interactor<TestPresenter, Router<TestInteractorB>>()
  private class TestRouterB : Router<TestInteractorB> {
    constructor(
      interactor: TestInteractorB,
      component: InteractorComponent<TestPresenter, TestInteractorB>,
    ) : super(component, interactor, getInstance(), Thread.currentThread()) {
      interactor.setPresenter(component.presenter())
    }

    constructor(
      component: InteractorComponent<TestPresenter, TestInteractorB>,
      interactor: TestInteractorB,
      ribRefWatcher: RibRefWatcher,
    ) : super(component, interactor, ribRefWatcher, Thread.currentThread()) {
      interactor.setPresenter(component.presenter())
    }
  }

  private class TestChildInteractor : Interactor<TestPresenter, Router<TestChildInteractor>>()
  private class TestChildRouter(
    interactor: TestChildInteractor,
    component: InteractorComponent<TestPresenter, TestChildInteractor>,
  ) : Router<TestChildInteractor>(component, interactor, getInstance(), Thread.currentThread()) {
    init {
      interactor.setPresenter(component.presenter())
    }
  }

  companion object {
    private const val TEST_KEY = "test_key"
    private const val TEST_VALUE = "test_value"
  }
}
